package net.plankton.mjpeg;

import java.awt.*;
import java.awt.image.*;
import java.io.*;
import java.util.*;

/**
 * look at:
http://www.opennet.ru/docs/formats/avi.txt
 */
public class AviGenerator {

	static byte[] dataSignature;

	static {
		dataSignature = new byte[4];
		dataSignature[0] = 48; // 0
		dataSignature[1] = 48; // 0
		dataSignature[2] = 100; // d
		dataSignature[3] = 99; // b
	}

	private JpegProvider jpeg;
	private RandomAccessFile raFile;
	private RandomAccessFile thumbFile;
	private int bytesPerPixel=0;

	private String dir;

	private int           xDim=0,yDim=0,zDim=0,tDim=0;

	private int           microSecPerFrame=0;
	private int           xPad;

	
	private int maxCount = 0;

	long savedbLength[];
	long[] saveLength;
	long time = 0;

	DataStructure data;

	public AviGenerator(JpegProvider provider, String dir) {
		jpeg = provider;
		this.dir=dir;
	}
	
	public AviGenerator(JpegProvider provider, String dir,int maxCount) {
		jpeg = provider;
		this.dir=dir;
		this.maxCount=maxCount;
	}

	int count=0;

	public void writeImage() throws Exception{
		
		time = System.currentTimeMillis();
		count++;
		if (maxCount>0 && count>maxCount) 
			count = 0;
		
		System.err.println(dir+" "+count);
		
		String fileName = dir+"-"+count +".avi";
		String jpegName = dir+"-"+count+".jpg";
		if (fileName == null)
			return;
		raFile = new RandomAccessFile(fileName, "rw");
		thumbFile = new RandomAccessFile(jpegName,"rw");
		String cF = dir+".counter";
		File f = new File(cF);
		f.delete();
		RandomAccessFile counterFile = new RandomAccessFile(cF, "rw");
		counterFile.write((""+count).getBytes());
		counterFile.close();
		data = new DataStructure(raFile);
		writeRiffChunk();
		writeHdrlChunk();
		writeStrlChunk();
		writeStrhSubChunk();
		writeStrfSubChunk();
		writeStrnSubChunk();
		data.writeLength(DataStructure.HDRL_SIZE);
		data.writeLength(DataStructure.STRL_SIZE);
		writeJunk();
		boolean ex =false;
		try {
			writeMoviChunk();
		}
		catch (Exception e) {
			ex =true;
		}
		writeIdxChunk();
		data.writeLength(DataStructure.RIFF_SIZE);
		data.writeLength(DataStructure.IDX_SIZE);
		raFile.close();
		
//		System.err.println(System.currentTimeMillis()-time);
		if (ex)
			throw new Exception();
		writeImage();

	}

	private void writeIdxChunk() throws IOException {
		int z;
		writeString("idx1");
		// Write the length of the idx1 CHUNK not including the idx1 signature and the 4 length
		// bytes. Write 0 for now.
		data.storePointer(DataStructure.IDX_SIZE);

		long gesSize = 0;
		long largestChunk=0;

		for (z = 0; z < zDim; z++) {
			gesSize+=saveLength[z];
			if (saveLength[z]>largestChunk)
				largestChunk=saveLength[z];
			// In the ckid field write the 4 character code to identify the chunk 00db or 00dc
			raFile.write(dataSignature);
			if (z == 0) {
				writeInt(0x10); // Write the flags - select AVIIF_KEYFRAME
			}
			else {
//				writeInt(0x00);
				writeInt(0x10);
			}
			// AVIIF_KEYFRAME 0x00000010L
			// The flag indicates key frames in the video sequence.
			// Key frames do not need previous video information to be decompressed.
			// AVIIF_NOTIME 0x00000100L The CHUNK does not influence video timing(for 
			//   example a palette change CHUNK).
			// AVIIF_LIST 0x00000001L Marks a LIST CHUNK.
			// AVIIF_TWOCC 2L
			// AVIIF_COMPUSE 0x0FFF0000L These bits are for compressor use.
			writeInt((int)(savedbLength[z]- 4 - data.getPointer(DataStructure.MOVI_START))); 
			// Write the offset (relative to the 'movi' field) to the relevant CHUNK
			writeInt((int)saveLength[z]); // Write the length of the relevant
			// CHUNK.  Note that this length is
			// also written at savedbLength
		}  // for (z = 0; z < zDim; z++)


//		endPos = raFile.getFilePointer();


		data.bookmark();
		data.restorePointer(DataStructure.BUFFER_SIZE);
		writeInt((int)largestChunk);
		data.restorePointer(DataStructure.IMGDATA_SIZE);
		writeInt((int)gesSize);
		data.back();


	}

	private void writeMoviChunk() throws Exception {
		int z;
		// Write the second LIST chunk, which contains the actual data
		writeString("LIST");
		// Write the length of the LIST CHUNK not including the first 8 bytes with LIST and
		// size.  The end of the second LIST CHUNK is followed by idx1.
		data.storePointer(DataStructure.MOVI_LENGTH);
		data.storePointer(DataStructure.MOVI_START);
		data.restorePointer(DataStructure.MOVI_START);

		writeString("movi"); // Write CHUNK type 'movi'
		savedbLength = new long[tDim*zDim];


		saveLength = new long[tDim*zDim];

		z=0;
		try {

			for (z=0;z<zDim;z++) {
				
				long diff = System.currentTimeMillis()-time;
				if (time>0 && diff>5000) {
					throw new Exception();
				}
				
				byte[] img = jpeg.next();
				raFile.write(dataSignature);

				int len = img.length;

				boolean added=false;
				if (len%2==1) {
					len++;
					added=true;
				}

				savedbLength[z] = raFile.getFilePointer();
				saveLength[z]=len;
				z++;

				writeInt(len);
				if (z==1) {
					thumbFile.write(img);
					thumbFile.close();
				}
				raFile.write(img);
				if (added)
					raFile.write(0);

			}
		}
		catch (Exception e) {
			e.printStackTrace();
			throw new Exception();
		}
		zDim=z;

		data.writeLength(DataStructure.MOVI_LENGTH);
	}

	private void writeJunk() throws IOException {
		int paddingBytes;
		int i;
		writeString("JUNK");
		paddingBytes = (int)(4084 - (data.getFile().getFilePointer() + 8));
		writeInt(paddingBytes);
		for (i = 0; i < (paddingBytes/2); i++) {
			writeShort((short)0);
		}
	}

	private void writeStrnSubChunk() throws IOException {
		byte[] text;
		writeString("strn");
		writeInt(16); // Write the length of the strn sub-CHUNK
		text = new byte[16];
		text[0] = 70; // F
		text[1] = 105; // i
		text[2] = 108; // l
		text[3] = 101; // e
		text[4] = 65; // A
		text[5] = 118; // v
		text[6] = 105; // i
		text[7] = 32; // space
		text[8] = 119; // w
		text[9] = 114; // r
		text[10] = 105; // i
		text[11] = 116; // t
		text[12] = 101; // e
		text[13] = 32; // space
		text[14] = 32; // space
		text[15] = 0; // termination byte
		raFile.write(text);
	}

	private void writeStrfSubChunk() throws IOException {

		writeString("strf"); // Write the stream format chunk
		// Write the size of the stream format CHUNK not including the first 8 bytes for
		// strf and the size.  Note that the end of the stream format CHUNK is followed by
		// strn.
		data.storePointer(DataStructure.STRF_SIZE);
		//changed from 40 / 64 /521
		writeInt(521); // biSize - Write header size of BITMAPINFO header structure
		// Applications should use this size to determine which BITMAPINFO header structure is 
		// being used.  This size includes this biSize field.
		writeInt(xDim-xPad);  // biWidth - image width in pixels
		writeInt(yDim);  // biHeight - image height in pixels.  If height is positive,
		// the bitmap is a bottom up DIB and its origin is in the lower left corner.  If 
		// height is negative, the bitmap is a top-down DIB and its origin is the upper
		// left corner.  This negative sign feature is supported by the Windows Media Player, but it is not
		// supported by PowerPoint.
		writeShort(1); // biPlanes - number of color planes in which the data is stored
		// This must be set to 1.
		int bitsPerPixel = (bytesPerPixel==3) ? 24 : 8;
		bitsPerPixel = 24;
		writeShort((short)bitsPerPixel); // biBitCount - number of bits per pixel #
		// 0L for BI_RGB, uncompressed data as bitmap
		//writeInt(bytesPerPixel*xDim*yDim*zDim*tDim); // biSizeImage #

		writeInt(1196444237); // biSizeImage #
//		biImageSize = raFile.getFilePointer();
//		writeInt(1244160); // biCompression - type of compression used
		data.storePointer(DataStructure.IMGDATA_SIZE);
		writeInt(0); // biXPelsPerMeter - horizontal resolution in pixels
		writeInt(0); // biYPelsPerMeter - vertical resolution in pixels
		// per meter
//		if (bitsPerPixel==8)
//		writeInt(256); // biClrUsed 
//		else
		writeInt(0); // biClrUsed 
		writeInt(0); // biClrImportant - specifies that the first x colors of the color table 
		// are important to the DIB.  If the rest of the colors are not available,
		// the image still retains its meaning in an acceptable manner.  When this
		// field is set to zero, all the colors are important, or, rather, their
		// relative importance has not been computed.
		// Write the LUTa.getExtents()[1] color table entries here.  They are written:
		// blue byte, green byte, red byte, 0 byte
//		if (bytesPerPixel==1) {
//		createLUT();
//		raFile.write(lutWrite);
//		}

		// Use strn to provide a zero terminated text string describing the stream
		data.writeLength(DataStructure.STRF_SIZE);
	}

	private void writeStrhSubChunk() throws IOException {
		writeString("strh"); // Write the strh sub-CHUNK
		writeInt(56); // Write the length of the strh sub-CHUNK
		writeString("vids"); // fccType - Write the type of data stream - here vids for video stream
		// Write DIB for Microsoft Device Independent Bitmap.  Note: Unfortunately,
		// at least 3 other four character codes are sometimes used for uncompressed
		// AVI videos: 'RGB ', 'RAW ', 0x00000000
		writeString("MJPG");
		writeInt(0); // dwFlags
		// 0x00000001 AVISF_DISABLED The stram data should be rendered only when 
		// explicitly enabled.
		// 0x00010000 AVISF_VIDEO_PALCHANGES Indicates that a palette change is included
		// in the AVI file.  This flag warns the playback software that it
		// will need to animate the palette.
		writeInt(0); // dwPriority - priority of a stream type.  For example, in a file with  
		// multiple audio streams, the one with the highest priority might be the 
		// default one.
		writeInt(0); // dwInitialFrames - Specifies how far audio data is skewed ahead of video
		// frames in interleaved files.  Typically, this is about 0.75 seconds.  In
		// interleaved files specify the number of frames in the file prior
		// to the initial frame of the AVI sequence.
		// Noninterleaved files should use zero.
		// rate/scale = samples/second
		writeInt(1); // dwScale



		writeInt(1000000/microSecPerFrame); //  dwRate - frame rate for video streams
		writeInt(0); // dwStart - this field is usually set to zero
		writeInt(tDim*zDim); // dwLength - playing time of AVI file as defined by scale and rate
		// Set equal to the number of frames
		writeInt(0); // dwSuggestedBufferSize - Suggested buffer size for reading the stream.
		// Typically, this contains a value corresponding to the largest chunk
		// in a stream.
		writeInt(-1); // dwQuality - encoding quality given by an integer between 
		// 0 and 10,000.  If set to -1, drivers use the default 
		// quality value.
		writeInt(0); // dwSampleSize #
		// 0 if the video frames may or may not vary in size
		// If 0, each sample of data(such as a video frame) must
		// be in a separate chunk.
		// If nonzero, then multiple samples of data can be grouped into
		// a single chunk within the file.
		// rcFrame - Specifies the destination rectangle for a text or video stream within the movie
		// rectangle specified by the dwWidth and dwHeight members of the AVI main header structure.
		// The rcFrame member is typically used in support of multiple video streams.  Set this 
		// rectangle to the coordinates corresponding to the movie rectangle to update the whole
		// movie rectangle.  Units for this member are pixels.  The upper-left corner of the destination
		// rectangle is relative to the upper-left corner of the movie rectangle.
		writeShort((short)0); // left
		writeShort((short)0); // top
		writeShort((short)0); // right
		writeShort((short)0); // bottom    
	}

	private void writeStrlChunk() throws IOException {
		writeString("LIST"); 
		data.storePointer(DataStructure.STRL_SIZE);
		writeString("strl");   
	}

	private void writeHdrlChunk() throws IOException {

		int xMod;
		writeString("LIST"); // CHUNK signature

		data.storePointer(DataStructure.HDRL_SIZE);
		writeString("hdrl"); // CHUNK type
		writeString("avih"); // Write the avih sub-CHUNK

		writeInt(0x38); // Write the length of the avih sub-CHUNK (38H) not including the
		// the first 8 bytes for avihSignature and the length
		//TODO seconds
		if (microSecPerFrame==0)
			microSecPerFrame = 40000;
		//IJ.write("microSecPerFrame: "+microSecPerFrame);
		writeInt(microSecPerFrame); // dwMicroSecPerFrame - Write the microseconds per frame

		writeInt(0); // dwMaxBytesPerSec
		// Write the maximum data rate of the file in bytes per second
		writeInt(0); // dwReserved1 - Reserved1 field set to zero
		//Took from other mjpeg
//		writeInt(2320);
//		writeInt(0);
		writeInt(0x10); // dwFlags - just set the bit for AVIF_HASINDEX
		// 10H AVIF_HASINDEX: The AVI file has an idx1 chunk containing
		//   an index at the end of the file.  For good performance, all
		//   AVI files should contain an index.
		// 20H AVIF_MUSTUSEINDEX: Index CHUNK, rather than the physical 
		// ordering of the chunks in the file, must be used to determine the
		// order of the frames.
		// 100H AVIF_ISINTERLEAVED: Indicates that the AVI file is interleaved.
		//   This is used to read data from a CD-ROM more efficiently.
		// 800H AVIF_TRUSTCKTYPE: USE CKType to find key frames
		// 10000H AVIF_WASCAPTUREFILE: The AVI file is used for capturing
		//   real-time video.  Applications should warn the user before 
		//   writing over a file with this fla set because the user
		//   probably defragmented this file.
		// 20000H AVIF_COPYRIGHTED: The AVI file contains copyrighted data
		//   and software.  When, this flag is used, software should not 
		//   permit the data to be duplicated.

		tDim = 1;
		//generic sizes!
		if (zDim==0)
			zDim = 100;
		if (yDim==0)
			yDim = 480;
		if (xDim==0)
			xDim = 640;
		xPad = 0;
		xMod = xDim%4;
		if (xMod != 0) {
			xPad = 4 - xMod;
			xDim = xDim + xPad;
		}

		writeInt(zDim*tDim); // dwTotalFrames - total frame number
		writeInt(0); // dwInitialFrames -Initial frame for interleaved files. 
		// Noninterleaved files should specify 0.
		writeInt(1); // dwStreams - number of streams in the file - here 1 video and zero audio.

		//buffer should be large enough to hold the largest chunk in the file
		data.storePointer(DataStructure.BUFFER_SIZE);
		writeInt(xDim-xPad); // dwWidth - image width in pixels
		writeInt(yDim); // dwHeight - image height in pixels
		// dwReserved[4] - Microsoft says to set the following 4 values to 0.
		writeInt(0);
		writeInt(0);
		writeInt(0);
		writeInt(0);
	}

	private void writeRiffChunk() throws IOException {
		writeString("RIFF"); 
		data.storePointer(DataStructure.RIFF_SIZE);
		writeString("AVI "); // RIFF type
	}


	final void writeString(String s) throws IOException {
		byte[] bytes =  s.getBytes("UTF-8");
		raFile.write(bytes);
	}

	final void writeInt(int v) throws IOException {
		raFile.write(v & 0xFF);
		raFile.write((v >>>  8) & 0xFF);
		raFile.write((v >>> 16) & 0xFF);
		raFile.write((v >>> 24) & 0xFF);
	}

	final void writeShort(int v) throws IOException {
		raFile.write(v& 0xFF);
		raFile.write((v >>> 8) & 0xFF);
	}

	/**
	 * @return the xDim
	 */
	public int getXDim() {
		return xDim;
	}

	/**
	 * @param dim the xDim to set
	 */
	public void setXDim(int dim) {
		xDim = dim;
	}

	/**
	 * @return the yDim
	 */
	public int getYDim() {
		return yDim;
	}

	/**
	 * @param dim the yDim to set
	 */
	public void setYDim(int dim) {
		yDim = dim;
	}

	/**
	 * @return the zDim
	 */
	public int getZDim() {
		return zDim;
	}

	/**
	 * @param dim the zDim to set
	 */
	public void setZDim(int dim) {
		zDim = dim;
	}

	/**
	 * @return the microSecPerFrame
	 */
	public int getMicroSecPerFrame() {
		return microSecPerFrame;
	}

	/**
	 * @param microSecPerFrame the microSecPerFrame to set
	 */
	public void setMicroSecPerFrame(int microSecPerFrame) {
		this.microSecPerFrame = microSecPerFrame;
	}

}
